Index: src/wp-includes/option.php
===================================================================
--- src/wp-includes/option.php	(revision 26979)
+++ src/wp-includes/option.php	(working copy)
@@ -451,7 +451,8 @@
 			$alloptions = wp_load_alloptions();
 			if ( !isset( $alloptions[$transient_option] ) ) {
 				$transient_timeout = '_transient_timeout_' . $transient;
-				if ( get_option( $transient_timeout ) < time() ) {
+				$transient_timeout_value = get_option( $transient_timeout );
+				if ( false !== $transient_timeout_value && $transient_timeout_value < time() ) {
 					delete_option( $transient_option  );
 					delete_option( $transient_timeout );
 					$value = false;
Index: tests/phpunit/tests/option/transient.php
===================================================================
--- tests/phpunit/tests/option/transient.php	(revision 26979)
+++ tests/phpunit/tests/option/transient.php	(working copy)
@@ -33,4 +33,22 @@
 		$this->assertEquals( $value, get_transient( $key ) );
 		$this->assertTrue( delete_transient( $key ) );
 	}
+
+	/**
+	 * According to ticket 23881, requesting a transient beginning with "timeout_" and appending
+	 * a valid transient key would inadvertently delete the timeout for the original transient.
+	 * This test recreates that workflow (as to verify that the bug has been patched).
+	 *
+	 * @ticket 23881
+	 */
+	function test_timeout_safe_delete() {
+		$key = 'test';
+		$value = '1234';
+
+		$this->assertTrue( set_transient( $key, $value, 60 * 60 ) );
+		get_transient( "timeout_{$key}" );
+
+		// Verify our assertion
+		$this->assertEquals( $value, get_transient( $key ) );
+	}
 }
