Set default HTTP timeout of 60s (#320)


diff --git a/describe.py b/describe.py
index 6d577de..e3881ae 100755
--- a/describe.py
+++ b/describe.py
@@ -34,7 +34,7 @@
 from googleapiclient.discovery import build
 from googleapiclient.discovery import build_from_document
 from googleapiclient.discovery import UnknownApiNameOrVersion
-import httplib2
+from googleapiclient.http import build_http
 import uritemplate
 
 CSS = """<style>
@@ -346,6 +346,7 @@
     print 'Warning: {} {} found but could not be built.'.format(name, version)
     return
 
+  http = build_http()
   response, content = http.request(
       uritemplate.expand(
           FLAGS.discovery_uri_template, {
@@ -366,7 +367,7 @@
   Args:
     uri: string, URI of discovery document.
   """
-  http = httplib2.Http()
+  http = build_http()
   response, content = http.request(FLAGS.discovery_uri)
   discovery = json.loads(content)
 
@@ -384,7 +385,7 @@
   if FLAGS.discovery_uri:
     document_api_from_discovery_document(FLAGS.discovery_uri)
   else:
-    http = httplib2.Http()
+    http = build_http()
     resp, content = http.request(
         FLAGS.directory_uri,
         headers={'X-User-IP': '0.0.0.0'})
diff --git a/googleapiclient/_auth.py b/googleapiclient/_auth.py
index 044ed12..87d3709 100644
--- a/googleapiclient/_auth.py
+++ b/googleapiclient/_auth.py
@@ -14,8 +14,6 @@
 
 """Helpers for authentication using oauth2client or google-auth."""
 
-import httplib2
-
 try:
     import google.auth
     import google.auth.credentials
@@ -31,6 +29,8 @@
 except ImportError:  # pragma: NO COVER
     HAS_OAUTH2CLIENT = False
 
+from googleapiclient.http import build_http
+
 
 def default_credentials():
     """Returns Application Default Credentials."""
@@ -86,6 +86,7 @@
     """
     if HAS_GOOGLE_AUTH and isinstance(
             credentials, google.auth.credentials.Credentials):
-        return google_auth_httplib2.AuthorizedHttp(credentials)
+        return google_auth_httplib2.AuthorizedHttp(credentials,
+                                                   http=build_http())
     else:
-        return credentials.authorize(httplib2.Http())
+        return credentials.authorize(build_http())
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
index 74f0a09..6291f34 100644
--- a/googleapiclient/discovery.py
+++ b/googleapiclient/discovery.py
@@ -61,6 +61,7 @@
 from googleapiclient.errors import UnacceptableMimeTypeError
 from googleapiclient.errors import UnknownApiNameOrVersion
 from googleapiclient.errors import UnknownFileType
+from googleapiclient.http import build_http
 from googleapiclient.http import BatchHttpRequest
 from googleapiclient.http import HttpMock
 from googleapiclient.http import HttpMockSequence
@@ -97,6 +98,7 @@
                     'version={apiVersion}')
 DEFAULT_METHOD_DOC = 'A description of how to use this function'
 HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH'])
+
 _MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40}
 BODY_PARAMETER_DEFAULT_VALUE = {
     'description': 'The request body.',
@@ -213,7 +215,10 @@
       'apiVersion': version
       }
 
-  discovery_http = http if http is not None else httplib2.Http()
+  if http is None:
+    discovery_http = build_http()
+  else:
+    discovery_http = http
 
   for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI,):
     requested_url = uritemplate.expand(discovery_url, params)
@@ -366,7 +371,7 @@
     # If the service doesn't require scopes then there is no need for
     # authentication.
     else:
-      http = httplib2.Http()
+      http = build_http()
 
   if model is None:
     features = service.get('features', [])
diff --git a/googleapiclient/http.py b/googleapiclient/http.py
index 0ef10b9..aece933 100644
--- a/googleapiclient/http.py
+++ b/googleapiclient/http.py
@@ -80,6 +80,8 @@
 
 _TOO_MANY_REQUESTS = 429
 
+DEFAULT_HTTP_TIMEOUT_SEC = 60
+
 
 def _should_retry_response(resp_status, content):
   """Determines whether a response should be retried.
@@ -1732,3 +1734,21 @@
 
   http.request = new_request
   return http
+
+
+def build_http():
+  """Builds httplib2.Http object
+
+  Returns:
+  A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
+  To override default timeout call
+
+    socket.setdefaulttimeout(timeout_in_sec)
+
+  before interacting with this method.
+  """
+  if socket.getdefaulttimeout() is not None:
+    http_timeout = socket.getdefaulttimeout()
+  else:
+    http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
+  return httplib2.Http(timeout=http_timeout)
diff --git a/googleapiclient/sample_tools.py b/googleapiclient/sample_tools.py
index 2b4e7b4..5ed632d 100644
--- a/googleapiclient/sample_tools.py
+++ b/googleapiclient/sample_tools.py
@@ -23,10 +23,10 @@
 
 
 import argparse
-import httplib2
 import os
 
 from googleapiclient import discovery
+from googleapiclient.http import build_http
 from oauth2client import client
 from oauth2client import file
 from oauth2client import tools
@@ -88,7 +88,7 @@
   credentials = storage.get()
   if credentials is None or credentials.invalid:
     credentials = tools.run_flow(flow, storage, flags)
-  http = credentials.authorize(http = httplib2.Http())
+  http = credentials.authorize(http=build_http())
 
   if discovery_filename is None:
     # Construct a service object via the discovery service.
diff --git a/tests/test__auth.py b/tests/test__auth.py
index 6711ffe..68e7aae 100644
--- a/tests/test__auth.py
+++ b/tests/test__auth.py
@@ -66,10 +66,13 @@
     def test_authorized_http(self):
         credentials = mock.Mock(spec=google.auth.credentials.Credentials)
 
-        http = _auth.authorized_http(credentials)
+        authorized_http = _auth.authorized_http(credentials)
 
-        self.assertIsInstance(http, google_auth_httplib2.AuthorizedHttp)
-        self.assertEqual(http.credentials, credentials)
+        self.assertIsInstance(authorized_http, google_auth_httplib2.AuthorizedHttp)
+        self.assertEqual(authorized_http.credentials, credentials)
+        self.assertIsInstance(authorized_http.http, httplib2.Http)
+        self.assertIsInstance(authorized_http.http.timeout, int)
+        self.assertGreater(authorized_http.http.timeout, 0)
 
 
 class TestAuthWithOAuth2Client(unittest2.TestCase):
@@ -112,11 +115,14 @@
     def test_authorized_http(self):
         credentials = mock.Mock(spec=oauth2client.client.Credentials)
 
-        http = _auth.authorized_http(credentials)
+        authorized_http = _auth.authorized_http(credentials)
 
-        self.assertEqual(http, credentials.authorize.return_value)
-        self.assertIsInstance(
-            credentials.authorize.call_args[0][0], httplib2.Http)
+        http = credentials.authorize.call_args[0][0]
+
+        self.assertEqual(authorized_http, credentials.authorize.return_value)
+        self.assertIsInstance(http, httplib2.Http)
+        self.assertIsInstance(http.timeout, int)
+        self.assertGreater(http.timeout, 0)
 
 
 class TestAuthWithoutAuth(unittest2.TestCase):
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index 3e26db9..47adeb7 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -65,6 +65,7 @@
 from googleapiclient.errors import UnacceptableMimeTypeError
 from googleapiclient.errors import UnknownApiNameOrVersion
 from googleapiclient.errors import UnknownFileType
+from googleapiclient.http import build_http
 from googleapiclient.http import BatchHttpRequest
 from googleapiclient.http import HttpMock
 from googleapiclient.http import HttpMockSequence
@@ -402,13 +403,32 @@
       discovery, base=base, credentials=self.MOCK_CREDENTIALS)
     self.assertEquals("https://www.googleapis.com/plus/v1/", plus._baseUrl)
 
-  def test_building_with_optional_http(self):
+  def test_building_with_optional_http_with_authorization(self):
     discovery = open(datafile('plus.json')).read()
     plus = build_from_document(
       discovery, base="https://www.googleapis.com/",
       credentials=self.MOCK_CREDENTIALS)
-    self.assertIsInstance(
-      plus._http, (httplib2.Http, google_auth_httplib2.AuthorizedHttp))
+
+    # plus service requires Authorization, hence we expect to see AuthorizedHttp object here
+    self.assertIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)
+    self.assertIsInstance(plus._http.http, httplib2.Http)
+    self.assertIsInstance(plus._http.http.timeout, int)
+    self.assertGreater(plus._http.http.timeout, 0)
+
+  def test_building_with_optional_http_with_no_authorization(self):
+    discovery = open(datafile('plus.json')).read()
+    # Cleanup auth field, so we would use plain http client
+    discovery = json.loads(discovery)
+    discovery['auth'] = {}
+    discovery = json.dumps(discovery)
+
+    plus = build_from_document(
+      discovery, base="https://www.googleapis.com/",
+      credentials=self.MOCK_CREDENTIALS)
+    # plus service requires Authorization
+    self.assertIsInstance(plus._http, httplib2.Http)
+    self.assertIsInstance(plus._http.timeout, int)
+    self.assertGreater(plus._http.timeout, 0)
 
   def test_building_with_explicit_http(self):
     http = HttpMock()
@@ -1316,7 +1336,7 @@
         zoo_uri = util._add_query_parameter(zoo_uri, 'userIp',
                                             os.environ['REMOTE_ADDR'])
 
-    http = httplib2.Http()
+    http = build_http()
     original_request = http.request
     def wrapped_request(uri, method='GET', *args, **kwargs):
         if uri == zoo_uri:
diff --git a/tests/test_http.py b/tests/test_http.py
index 36b43bf..512e8e1 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -43,6 +43,7 @@
 from googleapiclient.errors import BatchError
 from googleapiclient.errors import HttpError
 from googleapiclient.errors import InvalidChunkSizeError
+from googleapiclient.http import build_http
 from googleapiclient.http import BatchHttpRequest
 from googleapiclient.http import HttpMock
 from googleapiclient.http import HttpMockSequence
@@ -235,7 +236,7 @@
     def _postproc(*kwargs):
       pass
 
-    http = httplib2.Http()
+    http = build_http()
     media_upload = MediaFileUpload(
         datafile('small.png'), chunksize=500, resumable=True)
     req = HttpRequest(
@@ -1341,6 +1342,34 @@
     self.assertRaises(HttpError, request.execute)
 
 
+class TestHttpBuild(unittest.TestCase):
+  original_socket_default_timeout = None
+
+  @classmethod
+  def setUpClass(cls):
+    cls.original_socket_default_timeout = socket.getdefaulttimeout()
+
+  @classmethod
+  def tearDownClass(cls):
+    socket.setdefaulttimeout(cls.original_socket_default_timeout)
+
+  def test_build_http_sets_default_timeout_if_none_specified(self):
+    socket.setdefaulttimeout(None)
+    http = build_http()
+    self.assertIsInstance(http.timeout, int)
+    self.assertGreater(http.timeout, 0)
+
+  def test_build_http_default_timeout_can_be_overridden(self):
+    socket.setdefaulttimeout(1.5)
+    http = build_http()
+    self.assertAlmostEqual(http.timeout, 1.5, delta=0.001)
+
+  def test_build_http_default_timeout_can_be_set_to_zero(self):
+    socket.setdefaulttimeout(0)
+    http = build_http()
+    self.assertEquals(http.timeout, 0)
+
+
 if __name__ == '__main__':
   logging.getLogger().setLevel(logging.ERROR)
   unittest.main()